﻿using System;
using System.Collections.Generic;
using System.Xml;
using System.Web;
using System.IO;

using SharpObjects.Cryptography;
using SharpObjects.Cryptography.Symmetric;
using SharpObjects.PathMapping;
using SharpObjects.Utils;


namespace SharpObjects.Cryptography
{
    public class XmlCryptography
    {
        public static class Tags
        {
            public const string id = "id";
            public const string file = "file";
            public const string encryptionKeys = "encryption_keys";
            public const string encryptionKey = "encryption_key";
            public const string type = "type";
            public const string algorithm = "algorithm";
            public const string rijndael = "rijndael";
            public const string encryptionKeyReference = "encryption_key_reference";
            public const string isEncrypted = "is_encrypted";
        }

        public static class CDataUtil
        {

            private static readonly char[] XmlIllegalChars = new char[] { '<', '>', '&', '"', '\'', '/' };


            public static string CreateCDataString(string value)
            {
                return CreateCDataString(value, true);
            }

            public static string CreateCDataString(string value, bool bOptional)
            {
                if (bOptional)
                {
                    if (value.IndexOfAny(XmlIllegalChars) >= 0)
                    {
                        return string.Format("<![CDATA[{0}]]>", value);
                    }
                    else
                    {
                        return value;
                    }
                }
                else
                {
                    return string.Format("<![CDATA[{0}]]>", value);
                }
            }

            public static XmlNode CreateCDataNode(XmlDocument doc, string value)
            {
                return CreateCDataNode(doc, value, true);
            }

            public static XmlNode CreateCDataNode(XmlDocument doc, string value, bool isOptional)
            {
                if (isOptional)
                {
                    if (value.IndexOfAny(XmlIllegalChars) >= 0)
                    {
                        XmlCDataSection cdata = doc.CreateCDataSection(value);
                        return cdata;
                    }
                    else
                    {
                        XmlText text = doc.CreateTextNode(value);
                        return text;
                    }
                }
                else
                {
                    XmlCDataSection cdata = doc.CreateCDataSection(value);
                    return cdata;
                }
            }
        }


        public Dictionary<string, ICryptoProvider> Providers { get; set; }

        public ICryptoProvider DefaultProvider { get; set; }

        public IPathMapper PathMapper { get; set; }

        #region Constructors

        public XmlCryptography(IPathMapper pathMapper)
        {
            this.DefaultProvider = null;

            if (pathMapper != null)
            {
                this.PathMapper = pathMapper;
            }
            else
            {
                this.PathMapper = PathMapperFactory.PathMapper;
            }

            this.Providers = new Dictionary<string, ICryptoProvider>();
        }

        public XmlCryptography(Dictionary<string, ICryptoProvider> cryptoProviders, IPathMapper pathMapper)
        {
            this.DefaultProvider = null;

            if (pathMapper != null)
            {
                this.PathMapper = pathMapper;
            }
            else
            {
                this.PathMapper = PathMapperFactory.PathMapper;
            }

            this.Providers = cryptoProviders;
        }

        public XmlCryptography(XmlElement element, IPathMapper pathMapper)
        {
            this.DefaultProvider = null;

            if (pathMapper != null)
            {
                this.PathMapper = pathMapper;
            }
            else
            {
                this.PathMapper = PathMapperFactory.PathMapper;
            }

            this.Providers = new Dictionary<string, ICryptoProvider>();

            if (element != null)
            {
                XmlNodeList keynodes = element.SelectNodes("/*/" + Tags.encryptionKeys + "/" + Tags.encryptionKey);

                foreach (XmlElement keyelem in keynodes)
                {
                    string name = keyelem.GetAttribute(Tags.id);
                    ICryptoProvider provider = GetCryptoProvider(keyelem);
                    this.Providers[name] = provider;

                    if(this.DefaultProvider == null)
                    {
                        this.DefaultProvider = provider;
                    }
                }
            }
        }

        #endregion

        private ICryptoProvider FindProvider(XmlElement element)
        {
            if(element.HasAttribute(Tags.encryptionKeyReference))
            {
                string encryptionKeyName = element.GetAttribute(Tags.encryptionKeyReference);

                if (Providers.ContainsKey(encryptionKeyName))
                {
                    return Providers[encryptionKeyName];
                }
            }

            return this.DefaultProvider;
        }

        public bool IsElementEncrypted(XmlElement element)
        {
            if (element.HasAttribute(Tags.isEncrypted))
            {
                string isEncryptedValue = element.GetAttribute(Tags.isEncrypted);
                bool isEncrypted = Convert.ToBoolean(isEncryptedValue);
                return isEncrypted;
            }
            else
            {
                return false;
            }
        }

        public bool IsEncryptionEnabled(XmlElement element)
        {
            return element.HasAttribute(Tags.isEncrypted);
        }

        public string GetDecryptedXmlElementValue(XmlElement element, ref bool wasModified)
        {
            if (IsEncryptionEnabled(element))
            {
                ICryptoProvider provider = FindProvider(element);

                if (provider != null)
                {
                    if(!IsElementEncrypted(element))
                    {
                        EncryptXmlElementValue(element, ref wasModified);
                    }

                    Cryptographer crypt = new Cryptographer(provider);
                    string value = crypt.DecryptToUtf8Text(element.InnerText.Trim());
                    return value;
                }
                else
                {
                    throw new ApplicationException("No cryptography providers found");
                }
            }

            return element.InnerText;
        }

        public void RemoveXmlElementValueEncryption(XmlElement element, ref bool wasModified)
        {
            if (IsEncryptionEnabled(element))
            {
                ICryptoProvider provider = FindProvider(element);

                if (provider != null)
                {
                    if (IsElementEncrypted(element))
                    {
                        Cryptographer crypt = new Cryptographer(provider);
                        string value = crypt.DecryptToUtf8Text(element.InnerText.Trim());

                        element.InnerText = value;

                        for(int k = 0; k < element.Attributes.Count; k++)
                        {
                            XmlAttribute attribute = element.Attributes[k];

                            if(attribute.Name == Tags.isEncrypted)
                            {
                                element.Attributes.RemoveAt(k);
                                break;
                            }
                        }

                        for (int k = 0; k < element.Attributes.Count; k++)
                        {
                            XmlAttribute attribute = element.Attributes[k];

                            if (attribute.Name == Tags.encryptionKeyReference)
                            {
                                element.Attributes.RemoveAt(k);
                                break;
                            }
                        }

                        wasModified = true;
                    }
                }
                else
                {
                    throw new ApplicationException("No cryptography providers found");
                }
            }
        }

        public void AddXmlElementValueEncryption(XmlElement element, string encryptionKeyName, ref bool wasModified)
        {
            element.SetAttribute(Tags.encryptionKeyReference, encryptionKeyName);
            EncryptXmlElementValue(element, ref wasModified);
            wasModified = true;
        }

        public void EncryptXmlElementValue(XmlElement element, ref bool wasModified)
        {
            if (IsEncryptionEnabled(element))
            {
                ICryptoProvider provider = FindProvider(element);

                if (provider != null)
                {
                    Cryptographer crypt = new Cryptographer(provider);
                    XmlNode child = CDataUtil.CreateCDataNode(element.OwnerDocument, crypt.EncryptToBase64Text(element.InnerText));
                    element.InnerText = string.Empty;
                    element.AppendChild(child);
                    element.SetAttribute(Tags.isEncrypted, "True");
                    wasModified = true;
                }
                else
                {
                    throw new ApplicationException("No cryptography providers found");
                }
            }
        }

        public ICryptoProvider GetCryptoProvider(XmlElement keyelem)
        {
            ICryptoProvider prov = null;

            string algorithm = Tags.rijndael;

            if (keyelem.HasAttribute(Tags.algorithm))
            {
                algorithm = keyelem.GetAttribute(Tags.algorithm);
            }

            if (algorithm.ToLower() == Tags.rijndael)
            {
                prov = new RijndaelProvider();
            }
            else
            {
                throw new Exception("Unsupported cryptography algorithm: " + algorithm);
            }

            string keyType = Tags.file;

            if (keyelem.HasAttribute(Tags.type))
            {
                keyType = keyelem.GetAttribute(Tags.type);
            }

            if (keyType.ToLower() == Tags.file)
            {
                string file = this.PathMapper.Resolve(keyelem.InnerText.Trim());
                KeyFile kf = new KeyFile(file, prov);
                kf.Load();
            }
            else
            {
                throw new Exception("Unknown cryptography storage type: " + keyType);
            }

            return prov;
        }

        ////********************************************************************
        public string GetApplicationDataFolder()
        {
            string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            return path;
        }

        public void CreateCryptoKeyFile(string dir, string nameBase)
        {
            CreateCryptoKeyFile(dir + "\\" + CreateKeyFileName(nameBase));
        }

        public string CreateCryptoKeyFile(string path,bool mapToAppData = false, bool overwrite = false)
        {
            RijndaelProvider provider = new RijndaelProvider();

            if (mapToAppData)
            {
                path = this.PathMapper.MapApplicationDataPath(path);
            }

            string directory = Path.GetDirectoryName(path);

            if (directory != string.Empty)
            {
                Directory.CreateDirectory(directory);
            }

            if(overwrite)
            {
                KeyFile file = new KeyFile(path, provider);
                file.Save();
            }
            else
            {
                if (!File.Exists(path))
                {
                    KeyFile file = new KeyFile(path, provider);
                    file.Save();
                }
            }

            return path;
        }

        private string CreateKeyFileName(string encryptionKeyName)
        {
            return GetApplicationDataFolder() + encryptionKeyName + ".key";
        }


        ////********************************************************************


    }
}
